import atexit
import time
from pathlib import Path
import json5
import psutil
from YkPywebview import YkWebviewApi, start
import os, zipfile, subprocess
from tenacity import retry, stop_after_attempt, wait_fixed
import requests
from typing import Optional
from tkinter import Tk, filedialog
from auto import ConditionalTask, TaskCondition, TaskContext, TaskStep
from input import Input
import tkinter as tk
import pyautogui
import threading
import base64
import io
from yangke.common.config import logger
from gamepad import GamePadManager
from version import version


def capture_screen():
    """
    全局拖动截图功能，用户在屏幕上拖动选择区域，返回该区域的 base64 编码图像。

    Returns:
        str: 图像的 base64 字符串，格式为 data:image/png;base64,...
    """

    def _capture_thread():
        root = tk.Tk()
        root.overrideredirect(True)
        root.attributes('-alpha', 0.4)
        root.attributes('-topmost', True)
        root.geometry(f"{pyautogui.size().width}x{pyautogui.size().height}")
        root.configure(cursor="crosshair")

        canvas = tk.Canvas(root, bg='black', highlightthickness=0)
        canvas.pack(fill='both', expand=True)

        selection_rect = None
        size_text = None
        size_text_outline = None
        start_x, start_y = 0, 0
        end_x, end_y = 0, 0

        def on_close():
            root.destroy()

        def start_drag(event):
            nonlocal start_x, start_y, selection_rect, size_text, size_text_outline
            start_x, start_y = event.x, event.y
            selection_rect = canvas.create_rectangle(
                start_x, start_y, start_x, start_y,
                outline='red', width=2, dash=(4, 4)
            )
            size_text_outline = canvas.create_text(
                start_x + 5, start_y + 15,
                text="0 x 0",
                fill="black",
                font=("Arial", 14, "bold"),
                anchor="nw"
            )
            size_text = canvas.create_text(
                start_x + 5, start_y + 15,
                text="0 x 0",
                fill="white",
                font=("Arial", 14, "bold"),
                anchor="nw"
            )
            # 确保白色文字在黑色文字上方
            canvas.tag_raise(size_text, size_text_outline)

        def drag(event):
            nonlocal end_x, end_y
            if selection_rect:
                end_x, end_y = event.x, event.y
                canvas.coords(
                    selection_rect,
                    start_x, start_y,
                    end_x, end_y
                )

                # 更新尺寸文本的位置和内容
                width = abs(end_x - start_x)
                height = abs(end_y - start_y)

                # 将文本放在选区内部的左上角
                text_x = min(start_x, end_x) + 5
                text_y = min(start_y, end_y) + 5

                canvas.coords(size_text_outline, text_x, text_y)
                canvas.coords(size_text, text_x, text_y)
                canvas.itemconfig(size_text_outline, text=f"{width} x {height}")
                canvas.itemconfig(size_text, text=f"{width} x {height}")

        def end_drag(event):
            if selection_rect:
                # 隐藏选区矩形和尺寸文本，避免被截图
                canvas.itemconfigure(selection_rect, state='hidden')
                canvas.itemconfigure(size_text, state='hidden')
                canvas.itemconfigure(size_text_outline, state='hidden')

                # 强制更新画布
                root.update()

                # 短暂延迟确保UI更新完成
                time.sleep(0.1)

                # 截图
                x1, y1 = min(start_x, end_x), min(start_y, end_y)
                x2, y2 = max(start_x, end_x), max(start_y, end_y)

                screenshot = pyautogui.screenshot(region=(x1, y1, x2 - x1, y2 - y1))

                # 恢复显示选区矩形和尺寸文本
                canvas.itemconfigure(selection_rect, state='normal')
                canvas.itemconfigure(size_text, state='normal')
                canvas.itemconfigure(size_text_outline, state='normal')

                # 处理截图结果
                img_byte_arr = io.BytesIO()
                screenshot.save(img_byte_arr, format='PNG')
                img_byte_arr.seek(0)
                image_base64 = base64.b64encode(img_byte_arr.read()).decode('utf-8')

                # 返回结果
                result[0] = f"data:image/png;base64,{image_base64}"
                root.destroy()

        root.bind('<ButtonPress-1>', start_drag)
        root.bind('<B1-Motion>', drag)
        root.bind('<ButtonRelease-1>', end_drag)
        root.protocol("WM_DELETE_WINDOW", on_close)

        root.mainloop()

    result = [None]
    thread = threading.Thread(target=_capture_thread)
    thread.daemon = True
    thread.start()

    # 等待用户完成截图
    while result[0] is None:
        time.sleep(0.1)

    return result[0]


def deep_merge(d1, d2):
    """
    深度合并两个字典，d2 会覆盖 d1 的相同 key（递归）
    """
    for key in d2:
        if key in d1 and isinstance(d1[key], dict) and isinstance(d2[key], dict):
            deep_merge(d1[key], d2[key])
        else:
            d1[key] = d2[key]
    return d1


class Api(YkWebviewApi):
    _started_processes = []  # 类变量，保存所有启动的进程

    def __init__(self) -> None:
        super().__init__()
        self.gamepad = GamePadManager(self.printToTerm)
        self.input = Input()
        self.task: ConditionalTask | None = None
        # self.login_task = LoginTask()

    def capture_screen(self):
        """启动全局拖动截图，返回 base64 图像"""
        image = capture_screen()
        return {"suc": True, "image": image}

    @retry(stop=stop_after_attempt(3), wait=wait_fixed(2))  # 重试3次，每次间隔2秒
    def download_file(self, url):
        """
        下载文件并显示进度，失败时抛出异常以触发重试。
        """
        file_name = os.path.basename(url)
        file_path = os.path.join(os.getcwd(), file_name)

        if os.path.exists(file_path):
            self.printToTerm(f"文件已存在，跳过下载: {file_path}")
            return file_path  # 直接返回路径

        self.printToTerm(f"开始下载: {url}")
        response = requests.get(url, stream=True)

        total_size = int(response.headers.get('content-length', 0))
        downloaded_size = 0

        if response.status_code != 200:
            self.printToTerm(f"HTTP 错误: {response.status_code}")
            raise ConnectionError(f"下载失败，HTTP 状态码: {response.status_code}")

        try:
            with open(file_path, 'wb') as f:
                for chunk in response.iter_content(chunk_size=1024):
                    if chunk:
                        f.write(chunk)
                        downloaded_size += len(chunk)

                        if total_size > 0:
                            progress = (downloaded_size / total_size) * 100
                            self.setTaskBar(f"下载进度: {progress:.2f}%", progress)

            self.printToTerm(f"文件下载完成: {file_path}")
            return file_path

        except (IOError, requests.RequestException) as e:
            self.printToTerm(f"下载异常: {str(e)}")
            raise  # 抛出异常以便 tenacity 重试

    def find_file_in_folder(self, folder_path: str, file_name: str) -> Optional[str]:
        """
        在folder_path路径下查找名称为file_name的文件的绝对路径，并返回
        """
        for root, dirs, files in os.walk(folder_path):
            for file in files:
                if file == file_name:
                    return os.path.join(root, file)

        self.printToTerm(f"在目录{folder_path}下未找到名称为{file_name}的文件")
        return None

    def download_app(self, app_name: str, *args) -> dict:
        current_path = args[0].get("currentPath")
        if os.path.exists(current_path):
            self.setTaskBar(f"当前路径文件已存在，请勿重复安装: {current_path}")
            return {'suc': True, 'path': current_path}

        if app_name == "WebDM":
            dm_path = self.find_file_in_folder(os.getcwd(), "WebDM.exe")
            if dm_path is not None:
                return {'suc': True, 'path': dm_path}
            # url = "https://gitee.com/yangke02/webdm/releases/download/webdmv1.1/net8.0-windows10.0.22621.0.zip"
            url = "https://gitee.com/yangke02/webdm/releases/download/v2.1.0/win-x86.zip"
            file = self.download_file(url)
            # 解压到 webdm 文件夹
            webdm_dir = os.path.join(os.getcwd(), "webdm")
            os.makedirs(webdm_dir, exist_ok=True)

            with zipfile.ZipFile(file, 'r') as zip_ref:
                zip_ref.extractall(webdm_dir)
            self.printToTerm(f"{file} 已解压到: {webdm_dir}", "success")
            dm_path = self.find_file_in_folder(webdm_dir, "WebDM.exe")
            return {'suc': True, 'path': dm_path}
        elif app_name == "天谕":
            self.printToTerm('不支持直接下载游戏，请手动下载安装后选择运行文件')
        return {'suc': True, 'path': None}

    def select_file(self, title="选择文件", filetypes=(("所有文件", "*.*"),)) -> Optional[str]:
        """原有的文件选择方法保持不变"""
        root = Tk()
        root.withdraw()
        file_path = filedialog.askopenfilename(title=title, filetypes=filetypes)
        root.destroy()
        self.printToTerm(f"选择了{file_path}")
        return file_path if file_path else None

    def check_port_usage(self, port: int):
        """
        检查指定端口被哪个进程占用

        Args:
            port (int): 要检查的端口号

        Returns:
            Tuple: (进程名, PID, 执行路径) 或 None(如果端口未被占用)
        """
        for conn in psutil.net_connections():
            if conn.laddr.port == int(port):
                try:
                    proc = psutil.Process(conn.pid)
                    res = (proc.name(), proc.pid, proc.exe())
                    self.printToTerm(f"端口 {port} 已被 {res[0]} (PID: {res[1]}) 使用")
                    self.setTaskBar(f"端口 {port} 已被 {res[0]} (PID: {res[1]}) 使用")
                    return res
                except (psutil.NoSuchProcess, psutil.AccessDenied):
                    return f"Unknown(pid:{conn.pid})", conn.pid, ""
        return None

    def start_program(self, exe_file: str, *args) -> int:
        """
        启动服务程序并在主程序退出时自动终止

        Args:
            exe_file (str): 服务程序路径
            args (List[str]): 传递给程序的参数列表

        Returns:
            int: 启动的服务程序PID

        Raises:
            FileNotFoundError: 如果程序不存在
            subprocess.SubprocessError: 如果启动失败
        """
        if not os.path.exists(exe_file):
            raise FileNotFoundError(f"程序不存在: {exe_file}")

        if not os.access(exe_file, os.X_OK):
            raise PermissionError(f"无执行权限: {exe_file}")

        # 获取dm_path所在的目录作为工作目录
        working_dir = str(Path(exe_file).parent)
        program_name = os.path.basename(exe_file)
        print(f"启动程序: {program_name}")

        cmd = [exe_file]
        if args:
            cmd.extend(*args)

        try:

            # 启动程序并保存进程对象，指定工作目录
            process = subprocess.Popen(
                cmd,
                cwd=working_dir,  # 设置工作目录
                # stdout=subprocess.PIPE,  # 可选：重定向输出，重定向后，调试将不显示webdm.exe的日志
                # stderr=subprocess.PIPE  # 可选：重定向错误
            )
            self._started_processes.append({program_name: process})
            return process.pid
        except subprocess.SubprocessError as e:
            self.check_port_usage(8765)
            raise subprocess.SubprocessError(f"启动服务失败: {e}")

    def dm_start(self, dmPath=None, dmPort=None):
        """
        要在启动WebDM.exe时指定服务端口，需要修改appsettings.json中的端口号，或者删除appsettings.json文件后，使用--urls http://localhost:port指定。
        因为appsettings.json文件的优先级比命令行参数高，会覆盖命令行参数。
        :return:
        """
        if dmPath is None:
            dmPath = self.settings.get("WebDM", {}).get("path")
        else:
            self.settings = deep_merge(self.settings, {"WebDM": {"path": dmPath}})
        if dmPort is None:
            dmPort = self.settings.get("WebDM", {}).get("port")
        else:
            self.settings = deep_merge(self.settings, {"WebDM": {"port": dmPort}})

        assert dmPort is not None and dmPath is not None

        config_file = os.path.join(os.path.dirname(dmPath), "appsettings.json")
        process = self.check_port_usage(dmPort)
        if process is not None:
            if process[0].lower() == "webdm.exe":
                self.printToTerm("webdm.exe正在运行，请勿重复启动")
                self.setTaskBar("webdm.exe正在运行...")
                self.input.init('localhost', dmPort)
                return {"suc": True, "msg": "webdm.exe正在运行，请勿重复启动"}

        if os.path.exists(config_file):  # 判断appsettings.json文件是否存在，该文件优先级比命令行参数还高
            with open(config_file, 'r', encoding='utf-8') as f:
                config = json5.load(f)

            # 将config中的port值修改为dmPort
            http = config["Kestrel"]["Endpoints"]["Http"]  # {'Url': 'http://localhost:8765'}
            if http["Url"] != f"http://localhost:{dmPort}":
                http["Url"] = f"http://localhost:{dmPort}"
                with open(config_file, 'w', encoding='utf-8') as f:
                    json5.dump(config, f, indent=2, ensure_ascii=False, quote_keys=True)
            self.start_program(dmPath)
        else:
            self.start_program(dmPath, ["--urls", f"http://lcoalhost:{dmPort}"])

        self.input.init('localhost', dmPort)
        # self.input.wait_dm_ready()  # 等待dm插件加载完毕

        self.printToTerm("webdm.exe启动成功")
        self.setTaskBar("webdm.exe启动成功")
        return dmPath

    def exitWebDM(self, port):
        """
        退出占用端口port的进程
        :param port:
        :return:
        """
        process_info = self.check_port_usage(port)
        if process_info is None:
            self.printToTerm(f"端口 {port} 未被占用")
            self.setTaskBar(f"端口 {port} 未被占用")
            return {"suc": True, "msg": f"端口 {port} 未被占用"}

        process_name, pid, exe_path = process_info
        try:
            process = psutil.Process(pid)
            process.terminate()
            process.wait(timeout=5)  # 等待最多5秒

            self.printToTerm(f"已终止进程 {process_name} (PID: {pid})")
            self.setTaskBar(f"已终止进程 {process_name}")
            return {"suc": True, "msg": f"已终止进程 {process_name} (PID: {pid})"}
        except psutil.NoSuchProcess:
            self.printToTerm(f"进程 (PID: {pid}) 不存在或已退出")
            self.setTaskBar("进程不存在或已退出")
            return {"suc": True, "msg": f"进程 (PID: {pid}) 不存在或已退出"}
        except psutil.TimeoutExpired:
            # 如果正常终止超时，强制杀死进程
            try:
                process = psutil.Process(pid)  # 重新获取进程对象
                process.kill()
                self.printToTerm(f"强制终止进程 {process_name} (PID: {pid})")
                self.setTaskBar(f"强制终止进程 {process_name}")
                return {"suc": True, "msg": f"强制终止进程 {process_name} (PID: {pid})"}
            except Exception as e:
                self.printToTerm(f"无法终止进程 {process_name} (PID: {pid}): {str(e)}")
                self.setTaskBar(f"终止进程失败: {process_name}")
                return {"suc": False, "msg": f"无法终止进程 {process_name} (PID: {pid}): {str(e)}"}
        except Exception as e:
            self.printToTerm(f"终止进程 {process_name} (PID: {pid}) 失败: {str(e)}")
            self.setTaskBar(f"终止进程失败: {process_name}")
            return {"suc": False, "msg": f"终止进程 {process_name} (PID: {pid}) 失败: {str(e)}"}

    def dm_reg(self, registrationCode=None, additionalCode=None, dmPort=None):
        if dmPort is None:
            dmPort = self.settings.get("WebDM", {}).get("port")
        if registrationCode is None:
            registrationCode = self.settings.get("dm", {}).get("注册码")
        if additionalCode is None:
            additionalCode = self.settings.get("dm", {}).get("附加码")

        assert registrationCode is not None and additionalCode is not None and dmPort is not None
        return self.input.reg(registrationCode, additionalCode)

    def bind_game_window(self, hwnd):
        # hwnd = self.input.wait_for_exe_ready('天谕-无束缚3D幻想网游', 60, 1)
        self.input.UnBindWindow()  # 先解绑，再绑定
        self.input.SetWindowState(hwnd, 1)
        res = self.input.BindWindow(
            hwnd,
            "dx3",
            "dx.mouse.position.lock.api|dx.mouse.position.lock.message|dx.mouse.clip.lock.api|dx.mouse.input.lock.api|dx.mouse.state.api|dx.mouse.api|dx.mouse.cursor",
            "dx.keypad.input.lock.api|dx.keypad.state.api|dx.keypad.api",
            "dx.public.hide.dll|dx.public.graphic.protect|dx.public.anti.api|dx.public.km.protect|dx.public.active.api|dx.public.active.message",
            0
        )  # 公共参数dx.public.active.api|dx.public.active.message保证窗口处于激活状态（假激活）,使得SendString方法可以正常使用，有些窗口有判断是否在激活状态才接受输入文字
        self.printToTerm(f"BindWindow结果: {res}")

    def start_game(self, game_path):
        _ = self.start_program(exe_file=game_path)  # 启动软件，有时候启动进程和窗口的PID不一致，启动进程启动新进程后自己就退出了
        hwnd = self.input.wait_for_exe_ready('天谕')
        if hwnd == -1:  # 运行后，还是找不到窗口，则报错
            self.printToTerm(f"{game_path}启动失败")
            return {"success": False, "msg": f"{game_path}启动失败"}

        self.printToTerm(f'{game_path}的窗口ID为{hwnd}')
        if self.input.GetBindWindow() != 0:
            self.input.UnBindWindow()
            self.printToTerm('dm实例已经绑定了窗口，正在解绑')
            # return {"success": False, "msg": 'dm实例已经绑定了窗口，请先解绑'}

        hwnds = self.input.EnumWindow(0, '天谕', '#', 7)
        hwnd_child = 0
        for hwnd1 in hwnds:  # 查找带有子窗口title=play的窗口
            hwnd_child = self.input.FindWindowEx(hwnd1, 'null', 'play')
            if hwnd_child > 0:
                break
        hwnd = hwnd_child
        if hwnd <= 0:
            self.printToTerm(f"未找到目标窗口")
            return {"success": False, "msg": '未找到目标窗口'}
        res = self.input.BindWindow(
            hwnd,
            "dx3",
            "dx.mouse.position.lock.api|dx.mouse.position.lock.message|dx.mouse.clip.lock.api|dx.mouse.input.lock.api|dx.mouse.state.api|dx.mouse.api|dx.mouse.cursor",
            "dx.keypad.input.lock.api|dx.keypad.state.api|dx.keypad.api",
            "dx.public.hide.dll|dx.public.graphic.protect|dx.public.anti.api|dx.public.km.protect|dx.public.active.api|dx.public.active.message",
            101
        )  # 公共参数dx.public.active.api|dx.public.active.message保证窗口处于激活状态（假激活）,使得SendString方法可以正常使用，有些窗口有判断是否在激活状态才接受输入文字
        self.printToTerm(f"BindWindow结果: {res}")
        self.input.left_click(115, 27)  # 进入游戏按钮
        time.sleep(1)
        self.input.UnBindWindow()
        return {"success": True, "msg": '游戏启动成功'}

    def login_account(self, config=None):
        """
        config = {
          broker: QSName.value,
          account: QSAccount.value,
          password: QSPassword.value,
          appPath: QSExchangeAppPath.value
        }
        :param config:
        :return:
        """
        dmPort = self.settings.get("WebDM", {}).get("port")
        process = self.check_port_usage(dmPort)
        if process is not None:
            # 判断当前是否已经有绑定的窗口，如果有判断是否相同
            _ = self.input.GetBindWindow()
            if _ == self.input.hwnd:
                return {"success": True, "msg": "登录成功"}

        self.dm_start()  # 运行dm后台
        self.dm_reg()  # 注册dm
        self.setTaskBar("dm注册成功")
        self.printToTerm("dm注册成功")
        game_path = self.settings.get("game", {}).get("path")
        # 检查当前是否已经运行了qs_exchange_app_path对应的交易软件
        self.setTaskBar('检查是否存在启动器窗口')
        self.printToTerm(f"检查是否存在启动器窗口")
        hwnd = self.input.wait_for_exe_ready('天谕', 1)

        if hwnd <= 0:  # 当前没有运行，则启动
            self.setTaskBar(f"检查是否存在游戏窗口")
            self.printToTerm(f"检查是否存在游戏窗口")
            hwnd = self.input.FindWindowEx(0, 'null', '天谕-无束缚3D幻想网游')
            if hwnd > 0:
                self.setTaskBar(f"游戏已启动")
                self.printToTerm(f"游戏已启动")
            else:
                self.start_game(game_path)
                hwnd = self.input.wait_for_exe_ready('天谕-无束缚3D幻想网游', 100, 1)
                self.setTaskBar(f"游戏启动成功")
                self.printToTerm(f"游戏启动成功")

        self.printToTerm(f"开始绑定窗口：hwnd={hwnd}")
        self.setTaskBar(f"正在绑定游戏窗口")
        self.bind_game_window(hwnd)
        # user_name = config.get("account")
        # password = config.get("password")
        # self.login_task.run_with_interruption_handling()
        # self.login_task.execute_main_task(user_name, password, self.key_mouse)
        # print(user_name, password)
        return {"success": True, "msg": "登录成功"}

    def UnBindWindow(self) -> int:
        return self.input.UnBindWindow()

    def Capture(self):
        if self.input.port is None:
            self.input.init(self.settings.get("WebDM", {}).get("domain"), self.settings.get("WebDM", {}).get("port"))
        return self.input.capture()

    def TestCondition(self, condition_dict):
        """
        测试定义的单个条件

        :param condition_dict:
        :return:
        """
        self.login_account()

        condition = TaskCondition()
        condition.define_condition_by_json(condition_dict)

        context = TaskContext()
        context.setPrint(self.printToTerm, self.setTaskBar)
        context.input_interface = self.input
        logger.debug('测试条件开始')
        res = condition.test(context)
        logger.debug('测试条件结束')
        return res

    def testStep(self, step_dict):
        """
        测试定义的单个步骤

        :param step_dict: 值示例：{'executionCondition': {'conditionType': 'text_exists', 'region': 'x>0.75w, y<30', 'value': '离开中心服'}, 'executionMethods': [{'action': 'left_click', 'target': 'x+31, y+5'}], 'expectedResult': {'conditionType': 'text_exists', 'region': 'x>0.75w, y<30', 'value': '前往中心服'}, 'failureHandling': {'action': 'wait_and_retry', 'value': '1'}, 'id': '1759758106208-botps9742', 'name': '离开中心服', 'nextStepConditions': []}
        :return:
        """
        self.login_account()

        step = TaskStep()
        step.define_step_by_json(step_dict)
        context = TaskContext()
        context.setPrint(self.printToTerm, self.setTaskBar)
        context.input_interface = self.input
        logger.debug('测试步骤开始')
        step.execute(context)
        logger.debug('测试步骤结束')
        return True

    def TurnToTextObj(self, obj: str):
        """
        将角色的视角转向目标

        :param obj:
        :return:
        """
        base64_pic = self.input.capture(file='tmp.png').get('image')
        w, h = self.input.get_base64_image_size(base64_pic)
        ocr_res = self.input.ocr('tmp.png', target_str=obj, exact=True)
        # ocr_res = ('甜根草', [1019, 249, 1083, 275], 0.996487557888031)
        if isinstance(ocr_res, list):
            # 说明有多个目标，查找距离角色最近的目标
            pass
        elif isinstance(ocr_res, tuple):
            # 说明只有一个目标
            x1, y1, x2, y2 = ocr_res[1]
            cx, cy = int((x1 + x2) / 2), int((y1 + y2) / 2)
            self.input.right_down(cx, cy)
            time.sleep(0.2)
            self.input.move_to(int(w / 2), cy)
            self.input.right_up()
        else:  # 为None
            self.printToTerm(f"未找到目标{obj}")
        # self.input.capture(file='tmp.png', x1=732, y1=300, x2=957, y2=800)
        # ocr_res = self.input.ocr('tmp.png', target_str='卖萌的萝莉酱', exact=False)
        # if not isinstance(ocr_res, tuple):
        #     print("未找到角色")
        #
        print(ocr_res)

    def executeTask(self, task_data):
        """
        执行指定的任务
        :param task_data: 任务配置对象
        :return: 执行结果
        """
        try:
            # 创建条件任务对象
            self.task = ConditionalTask(task_data['name'])
            self.task.timer = task_data.get('timer', [])
            self.task.count = task_data.get('count', [])
            self.task.context.setPrint(self.printToTerm, self.setTaskBar)  # 设置打印和任务栏更新方法，这样任务中执行的函数会调用这两个方法
            # 使用新的方法从JSON数据定义任务步骤
            self.task.define_tasks_json(task_data['steps'])

            # 设置输入接口
            self.task.set_input_interface(self.input)

            self.printToTerm(f"开始执行任务: {task_data['name']}")
            self.setTaskBar(f"开始执行任务: {task_data['name']}")

            # 使用run_with_interruption_handling方法执行任务
            self.task.run_with_interruption_handling()

            self.printToTerm(f"任务 {task_data['name']} 执行完成", "success")
            self.setTaskBar(f"任务 {task_data['name']} 执行完成")
            return {"success": True, "msg": "任务执行完成"}
        except Exception as e:
            self.printToTerm(f"任务执行过程中出现异常: {str(e)}", "error")
            self.setTaskBar("任务执行异常")
            return {"success": False, "msg": f"任务执行异常: {str(e)}"}

    def stopTask(self):
        """
        停止任务，同时取消绑定窗口

        :return:
        """
        if self.task is not None:
            self.input.UnBindWindow()
            self.task.request_stop()

    def insert_game_pads(self, mode, keys, key_press_settings):
        """
        添加两个虚拟游戏手柄实例

        :return:
        """
        self.set_gamepad_mode(mode)
        self.set_gamepad_keys(keys)
        self.set_timed_key_press_settings(key_press_settings)
        self.gamepad.insert_game_pads()
        self.gamepad.start_listening()  # 停止模拟手柄，只需要按下F4键，即恢复键鼠操作的快捷键

    def set_gamepad_mode(self, mode):
        """
        设置手柄操作模式的快捷键

        :param mode:
        :return:
        """
        self.gamepad.set_gamepad_mode(mode)
        self.setProjectSettings({"gamepad": {"mode": mode}})

    def set_gamepad_keys(self, keys):
        self.gamepad.set_gamepad_keys(keys)
        self.setProjectSettings({"gamepad": {"keys": keys}})

    def set_timed_key_press_settings(self, key_press_settings):
        self.gamepad.set_timed_key_press_settings(key_press_settings)
        self.setProjectSettings({"timedKeyPress": key_press_settings})

    def start_gamepad(self, gamepad: int = 1):
        """
        开始虚拟游戏手柄检测，全局监听键鼠操作，转换为游戏手柄的操作，模拟游戏手柄操作
        :return:
        """
        self.gamepad.start_listening()

    def stop_gamepad(self, gamepad: int = 1):
        """
        停止虚拟游戏手柄监听

        :param gamepad: 1或2，表示要停止的虚拟游戏手柄实例
        :return:
        """
        self.gamepad.stop_listening()

    @classmethod
    def cleanup_processes(cls):
        """退出时清理所有启动的进程"""
        for proc in cls._started_processes:
            for program_name, process in proc.items():
                print(f"正在清理进程: {program_name}")
                if process.poll() is None:  # 检查是否仍在运行
                    print(process)
                    if isinstance(process, subprocess.Popen):
                        process.terminate()  # 终止进程
                        process.wait()  # 等待进程结束
                    elif isinstance(process, dict):
                        print(process)


# 注册退出时的清理函数
atexit.register(Api.cleanup_processes)

if __name__ == '__main__':
    start(Api, url='./dist/index.html', debug=False, title=f'手柄模拟器-{version}')
